Overview
Sigil plugin that converts CSS font-size keywords (xx-small, x-small, small, medium, large, x-large, xx-large, smaller, larger) to em or rem units across CSS files, embedded <style> blocks, and inline style="" attributes.
Core Requirements
Conversion Targets
- CSS Files: External .css stylesheets
- Embedded Styles:
<style>blocks in HTML/XHTML headers - Inline Styles:
style=""attributes on HTML elements
Conversion Patterns
Primary (Always Active)
- Pattern:
font-size: <keyword>; - Safe and reliable for all contexts
Secondary (Optional)
- Pattern:
font: <keyword> <font-family>;and variations - User must explicitly enable
- Requires validation and preview workflow
Keyword Mappings (Defaults)
| Keyword | Default Value | Default Unit |
|---|---|---|
| xx-small | 0.6 | em |
| x-small | 0.75 | em |
| small | 0.89 | em |
| medium | 1 | em |
| large | 1.2 | em |
| x-large | 1.5 | em |
| xx-large | 2 | em |
| smaller | 0.85 | em |
| larger | 1.15 | em |
Each keyword independently configurable for numeric value (positive float) and unit type (em or rem).
Architecture
Entry Point
def run(bk):
"""
Main plugin entry point called by Sigil
Args:
bk: Sigil book container object
Returns:
0 on success, -1 on error
"""
Key Components
1. Configuration Management
Storage Location: ~/.sigil/NewUnit/configs/
File Format: JSON
{
"plugin": "FontSizeKeywordToEm",
"version": "0.7.0",
"name": "User Config Name",
"description": "Optional description",
"created": "2025-01-15T10:30:00",
"config": {
"xxsmall": "0.6",
"xxsmall_unit": "em",
"convert_font_shorthand": "true",
"create_backup": "false",
"backup_path": "/path/to/backup"
}
}
Types:
- Default preferences (stored in Sigil prefs)
- Named configurations (JSON files)
Validation:
- Folder existence and creation
- Write permissions testing
- Numeric value validation (positive floats only)
2. Comment Handling
def strip_css_comments(css_text):
"""
Remove CSS comments and return text with placeholders
Returns:
(text_no_comments, placeholder_map)
"""
placeholder_map = {}
def save_comment(match):
placeholder = f"___CSS_COMMENT_{uuid.uuid4().hex}___"
placeholder_map[placeholder] = match.group(0)
return placeholder
text_no_comments = re.sub(r'/\*.*?\*/', save_comment, css_text,
flags=re.DOTALL)
return text_no_comments, placeholder_map
3. Safety Detection
def is_unsafe_shorthand(css_line):
"""
Detect potentially unsafe font shorthand patterns
Args:
css_line: CSS line containing font property
Returns:
True if unsafe, False if safe
"""
# Remove comments for accurate checking
line_no_comments = re.sub(r'/\*.*?\*/', '', css_line)
# Extract ONLY the font property value
font_match = re.search(r'\bfont\s*:\s*([^;{}"\']+)',
line_no_comments, re.IGNORECASE)
if not font_match:
return False
font_value = font_match.group(1)
# Check for complex functions IN THE VALUE
if re.search(r'\b(calc|var|min|max|clamp)\s*\(',
font_value, re.IGNORECASE):
return True
# Check for multiple slashes
if font_value.count('/') > 1:
return True
# Check for excessive complexity
parts = re.split(r'\s+', font_value.strip())
if len(parts) > 8:
return True
return False
4. Backup System
Memory Backup (Always Enabled):
backup_files = {}
for file_info in selected_files:
original_text = bk.readfile(file_info['id'])
if not isinstance(original_text, str):
original_text = original_text.decode('utf-8')
backup_files[file_info['id']] = {
'content': original_text,
'href': file_info['href']
}
External Backup (Optional):
if create_backup:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
for file_info in selected_files:
original_text = bk.readfile(file_info['id'])
base_name = os.path.basename(file_info['href'])
name, ext = os.path.splitext(base_name)
backup_filename = f"{name}_{timestamp}{ext}"
backup_filepath = os.path.join(backup_path, backup_filename)
with open(backup_filepath, 'w', encoding='utf-8') as f:
f.write(original_text)
UI Specifications
1. File Selector Window (Main)
Dimensions: Responsive
- Minimum: 700x550
- Maximum: 60% screen width, 70% screen height
- Resizable: Yes
2. Configuration Window
Dimensions: Responsive (Minimum: 750x800, Maximum: 80% screen width/height)
3. Preview Window
Dimensions: 800x600 (fixed)
Color Coding
| Element | Background | Text | Notes |
|---|---|---|---|
| Regular "before" | #ffd6d6 (pink) | #000000 | Font-size changes |
| Shorthand "before" | #cce5ff (blue) | #000000 | With [SHORTHAND] prefix |
| [SHORTHAND] label | same as line | #FF0000 (red) | Bold font |
| "after" (all) | #d6ffd6 (green) | #000000 | Both types |
| File headers | none | #2196F3 (blue) | Bold, larger font |
Conversion Workflow
Phase 1: Initialization
- Load preferences from Sigil
- Get saved configuration values
- Use defaults for missing values
- Store in config dictionary
- Build regex patterns from config
- Scan all CSS and text files
- Display file selector window
Phase 2: User Selection
- Show file selector with checkboxes
- If Preview clicked: Build and show preview
- If Apply clicked: Continue to Phase 3
Phase 3: Backup
External Backup: Optional, with folder validation
Phase 4: Conversion
For each selected file:
- Retrieve original from memory backup
- Strip CSS comments
- Apply patterns
- Restore CSS comments
- Write modified content
- Track changes and errors
Phase 5: Results & Undo
Display summary and enable undo functionality to restore from memory backup
Regular Expression Patterns
Font-Size Pattern
pattern = rf'(font-size\s*:\s*){re.escape(keyword)}(\s*(?=[;}}"\'\!]|$))'
Example transformation:
Input: font-size: small;
Output: font-size: 0.89em;
Font Shorthand Pattern (If Enabled)
pattern = rf'(\bfont\s*:\s*(?:[^\s;{{}}]+\s+)*?){re.escape(keyword)}(\s*(?:[/\s][^\s;{{}}]+)*\s*(?=[;}}"\']|$))'
Example transformations:
Input: font: small Arial;
Output: font: 0.89em Arial;
Input: font: bold small/1.5 Georgia;
Output: font: bold 0.89em/1.5 Georgia;
Pattern Flags
Both patterns use: re.IGNORECASE | re.MULTILINE
Test Cases
Straightforward Cases (Should Convert)
.test-small { font-size: small; }
.compact{font-size:small;}
.uppercase { font-size: SMALL; }
.important { font-size: small !important; }
Font Shorthand Safe (If Enabled)
.basic { font: small Arial; }
.style { font: italic small serif; }
.weight { font: bold small monospace; }
.line-height { font: small/1.5 Arial; }
Font Shorthand Unsafe (Should Skip)
.calc { font: calc(1em + 2px) small Arial; }
.var { font: var(--my-size) small Arial; }
.min { font: min(12px, 1em) small Arial; }
.clamp { font: clamp(12px, 1em, 20px) small Arial; }
Should NOT Convert
/* Wrong properties */
.bg { background-size: small; }
/* Already converted */
.px { font-size: 12px; }
.em { font-size: 1em; }
/* System keywords */
.inherit { font-size: inherit; }
/* In comments */
/* font-size: small; */
Critical Implementation Notes
1. UUID-Based Placeholders
CRITICAL: Must use uuid.uuid4().hex, not sequential numbers.
Why: Sequential numbers like ___COMMENT_0___ could exist in CSS. Collision would corrupt CSS during restoration.
2. Regex Keyword Escaping
CRITICAL: Always re.escape() keywords before inserting into regex.
Why: Future-proofs against adding unusual keywords and is standard security practice.
3. Comment Scope in is_unsafe_shorthand()
CRITICAL: Extract and check ONLY the font property value, not the entire line.
Why: Comments might contain keywords like calc, var. Only care about what's inside the font property value.
4. Dialog Workflow Order
CRITICAL: Unsafe → Safe shorthand → Apply (in that order).
Why: User sees warnings before making decisions. Separates acknowledgment from choice.
5. Backup Priority
CRITICAL: Memory backup ALWAYS happens; external backup is optional.
Why: Memory backup enables undo within session. External backup provides crash recovery.
Dependencies
Required
- Python: 3.6+ (f-strings used)
- Standard Library: re, sys, os, json, uuid, datetime, tkinter
- No third-party dependencies - Plugin is self-contained
Sigil API
| Method | Description |
|---|---|
bk.getPrefs() | Returns dict |
bk.savePrefs(dict) | Saves dict |
bk.css_iter() | Returns iterator of (id, href) |
bk.text_iter() | Returns iterator of (id, href) |
bk.readfile(id) | Returns bytes or str |
bk.writefile(id, str) | Writes str to file |
Compatibility: Sigil 1.0+